# gbconv deamon program file.
# Grown by LiuZhiyan.
# http://lzy.javaeye.com/
# lzy.dev@gmail.com

require 'webrick/httpproxy'
require 'open-uri'
require 'stringio'
require 'yaml'
require 'zlib'
require 'iconv'
require 'hpricot'
require 'win32/process' # Windows platform-dependent. You can remove this require file on *nix platform directly.

require 'gbconv_comm.rb'

# Deveice "nul" for windows (dos) deamon only, but "/dev/null" for *nux.
$stdin.reopen('nul')
$stdout.reopen('nul')
$stderr.reopen('nul')

class GbConv
	def initialize(conf_file = conf_default_filepath)
		File.open(pid_filepath, "w") { |f| f.write(Process.pid) }
		
		# Setup log configuration.
		@log_filepath = log_default_filepath
		conf(conf_file)
		@log_filepath = @config[:LogFile] if @config[:LogFile]

		log_event("Initializing http proxy server...")

		@server = WEBrick::HTTPProxyServer.new(@config[:Proxy])

		log_event("Server initialized.")
	end

	# Should not log nothing in this function because log configuration was not loaded yet.
	def conf(conf_file)
		if FileTest.exists?(conf_file)
			File.open(conf_file) { |f| @config = YAML.load(f) }
		else
			msg = "Configuration file #{conf_file} not found, exiting."
		
			log_event(msg)
			puts(msg)
		
			exit
		end
	
		@config[:Proxy][:ProxyContentHandler] = method(:content_handler)
	end

	def log_event(msg)
		File.open(@log_filepath, "a+") { |f|
			f.seek(0, IO::SEEK_END)
			f.puts("#{Time.now.strftime("%Y %b %d %H:%M:%S")}: #{msg}")
		}
	end

	def skip?(req, res)
		# Skip filting on this request default.
		result = { :skip => true, :gzip => false, :body => res.body }

		# Check response body is exist.
		return result unless res.body

		# Skip none text/html resource.
		encoding = res.header['content-type']
		return result if (encoding && !(encoding =~ /text\/html/i))

		# Decompression when body compressed by gzip.
		encoding = res.header['content-encoding']
		if (encoding && (encoding =~ /gzip/i))
			result[:gzip] = true
			result[:body] = Zlib::GzipReader.new(StringIO.new(result[:body])).read
		end

		# Get meta information from text/html resource.
		chset = charset(result[:body])
		return result unless chset

		# Skip UTF-8 coding text/html resource.
		return result if (chset =~ /utf-8/i)
		
		result[:skip] = false
		
		return result
	end

	def content_handler(req, res)
=begin
		# log headers for debug.

		log_event("res.headers: ");
		res.header.each { |k, v| log_event("#{k} => #{v}") }
		log_event("res.headers list end.");
=end

		# Check is filting skip.
		check_result = skip?(req, res)
		return log_event("Skip '#{req.request_uri}'.") if check_result[:skip]

		# Update charset property.
		res.header['content-type'] = 'text/html; charset=utf-8'
		check_result[:body] = set_charset(check_result[:body], 'utf-8')

		# Convert text/html resource from gb to utf-8 coding.
		if check_result[:gzip]
			begin
				gz = Zlib::GzipWriter.new(StringIO.new(res.body))
				gz.write(iconv_to_utf8(check_result[:body]))
			ensure
				gz.close unless gz.nil?
			end
		else
			res.body = iconv_to_utf8(check_result[:body])
		end
		
		res.header['content-length'] = nil
		
		log_event("Process '#{req.request_uri}'.")
	end

	def charset(html)
		# Search by xpath expression.
		Hpricot(html).search("/html/head/meta[@http-equiv.contains_lowercase('content-type')][@content]").each { |e|
			return $1 if e.attributes['content'] =~ /charset=(.+);?/i
		}

		nil
	end

	def set_charset(html, cs)
		doc = Hpricot(html)

		# Search by xpath expression.
		(doc/"/html/head/meta[@http-equiv.contains_lowercase('content-type')][@content]").set(:content, "text/html; charset=#{cs};")
			
		doc.to_s
	end

	def iconv_to_utf8(gb_str)
		gb_code = @config[:GbCode]
		
		unless gb_code
			msg = "GB code configuration for convert in #{conf_file} file not found, exiting."
		
			log_event(msg)
			puts(msg)

			exit
		end
	
		begin
			gb_str ? Iconv.iconv("UTF-8//IGNORE", gb_code, gb_str) : gb_str
		rescue
			gb_str
		end
	end

	def start
		log_event("Starting http proxy server...")

		@server.start

		log_event("Server started.")
	end

	def stop
		log_event("Stopping http proxy server...")
		
		@server.stop

		log_event("Server stopped.")
	end
end

pid = fork do
	# Setup deamon common configuration.
	Dir.chdir(workdir_path)
	File.umask(0)
	#Process.setsid
	
	gbconv = GbConv.new
	
	begin
		# SIGHUP signal unsuppourted on windows platform.
		# Use SIGHUP on *nix, but SIGINT(signal 2) on windows platform.
		trap(2) { gbconv.stop }
	rescue => e
		gbconv.log_event("Setup signal trap failed: #{e}. Startup has been interrupted.")
	else
		gbconv.start
	end
end

# Process::fork method in "win32/process" extension call while returns twice though a block is specified.
# Once in the parent, returning the process ID of the child, and once in the child, returning -1.
# Otherwise, returning value zero when child process exit.
# There are especial in "win32/process" extension on windows platform. You should remove this code directly on *nix platform.
exit! if pid < 1

Process.detach(pid) if pid
